package testtiles;

import java.awt.Graphics2D;

import butines.core.math2d.Vector2D;
import butines.core.math2d.Vector2DUtils;
import butines.game.Animation;
import butines.game.GameObject;
import butines.game.Message;
import butines.game.State;
import butines.game.StateMachine;
import butines.game.steering.SteerLibrary;
import butines.game.steering.Vehicle;

public class Unit implements GameObject {

	private UnitClass unitClass;

	private StateMachine<Unit> stateMachine = new StateMachine<Unit>(this);
	
	private World world;
	
	private Vector2D heading = new Vector2D();
	private Vector2D velocity = new Vector2D();
	private Vector2D position = new Vector2D();
	
	private Vector2D targetPoint = new Vector2D();
	private float distance = 0;
	
	private int health;
	private boolean alive;
	private float waitCount;
	
	private Animation currentAnimation;

	private AnimationSet animationSet; 
	
	private Steering steering = new Steering();
	
	public Unit(UnitClass unitClass) {
		if (unitClass == null) {
			throw new IllegalArgumentException("Argument 'unitClass' is null");
		}
		
		this.unitClass = unitClass;
		
		health = unitClass.getMaxHealth();
		alive = true;
		
		animationSet = new AnimationSet();
		animationSet.getStay().play();
		
	}
	
	public int getHealth() {
		return health;
	}

	public void setHealth(int health) {
		this.health = health;
	}

	public boolean isAlive() {
		return alive;
	}

	public void setAlive(boolean alive) {
		this.alive = alive;
	}
	
	public Vector2D getPosition() {
		return position;
	}
	
	public void setPosition(Vector2D position) {
		this.position.set(position);
	}
	
	public void setPosition(float x, float y) {
		this.position.set(x, y);
	}
	
	public Vector2D getVelocity() {
		return velocity;
	}

	public void setVelocity(Vector2D velocity) {
		this.velocity.set(velocity);
	}
	
	public float getMaxSpeed() {
		return unitClass.getMaxSpeed();
	}
	
	public float getRadiusBounding() {
		return unitClass.getRadiusBounding();
	}
	
	public float getViewDistance() {
		return unitClass.getViewDistance();
	}
	
	public float getSpeed() {
		return steering.getSpeed();
	}
	
	public void setSpeed(float speed) {
		if (speed == 0) {
			this.velocity.set(0, 0);
		} else if (speed > 0) {
			this.velocity.normalise().mult(Math.min(speed, getMaxSpeed()));
		} else {
			throw new IllegalArgumentException("speed is negative");
		}
	}

	public boolean enforceNonPenetrationConstraint(Unit other) {
		return steering.enforceNonPenetrationConstraint(other);
	}
	
	public Vector2D getHeading() {
		return heading;
	}

	public void setHeading(Vector2D heading) {
		this.heading.set(heading);
	}
	
	public Vector2D getTargetPoint() {
		return targetPoint;
	}

	public void setTargetPoint(Vector2D targetPoint) {
		setTargetPoint(targetPoint.x, targetPoint.y);
	}
	
	public void setTargetPoint(float x, float y) {
		targetPoint.set(x, y);
		distance = Vector2DUtils.distance(position, targetPoint) * 1.3f;
	}
	
	public boolean atTargetPoint() {
		return Math.round(position.x) == Math.round(targetPoint.x)
			&& Math.round(position.y) == Math.round(targetPoint.y)
			|| distance <= 0;
	}
	
	public float getDistance() {
		return distance;
	}
	
	public World getWorld() {
		return world;
	}

	public void setWorld(World world) {
		this.world = world;
	}
	
	public void setWaitCount(float waitCount) {
		this.waitCount = waitCount;
	}
	
	public float getWaitCount() {
		return waitCount;
	}
	
	public StateMachine<Unit> getStateMachine() {
		return stateMachine;
	}

	public void changeState(State<Unit> newState) {
		stateMachine.changeState(newState);
	}

	public AnimationSet getAnimationSet() {
		return animationSet;
	}
	
	public Animation getCurrentAnimation() {
		return currentAnimation;
	}
	
	private void setCurrentAnimation(Animation anim) {
		if (anim == null) {
			return;
		}
		if (currentAnimation == anim && currentAnimation.isEnded() == false) {
			return;
		}
		currentAnimation = anim;
		currentAnimation.start();
	}
	
	public Steering getSteering() {
		return steering;
	}
	
	// implements GameObject
	public int getId() {
		return 0;
	}

	public boolean handleMessage(Message message) {
		return stateMachine.handlerMessage(message);
	}

	public void update(float tpf) {
		if (waitCount > 0) {
			waitCount = (waitCount - tpf > 0) ? waitCount - tpf : 0;
		}
		
		if (distance > 0) {
			distance -= getSpeed();
		}

		stateMachine.update();
		steering.updatePosition();
		
		if (world != null) {
			world.enforceNonPenetrationContraint(this);		
		}
	
		if (currentAnimation != null) {
			currentAnimation.update(tpf, heading.theta());
		}
	}

	public void draw(Graphics2D g2d) {
		if (currentAnimation != null) {
			currentAnimation.draw(g2d, position.x, position.y);
		}
	}

	public void release() {
		// TODO Auto-generated method stub
	}
	
	public class AnimationController {

		private Animation animation;

		public AnimationController( Animation animation) {
			this.animation = animation;
		}

		public void play() {
			setCurrentAnimation(animation);
		}
		
		public boolean isPlaying() {
			return currentAnimation != null && currentAnimation == animation;
		}
		
		public boolean isStopped() {
			return isPlaying() && currentAnimation.isStopped();
		}
		
		public boolean isEnded() {
			return isPlaying() && currentAnimation.isEnded();
		}
		
	}
	
	public class AnimationSet {
		
		private AnimationController stay;
		private AnimationController walk;
		private AnimationController attack;
		private AnimationController die;
		
		public AnimationSet() {
			stay = new AnimationController(unitClass.getStay().copy());
			walk =  new AnimationController(unitClass.getWalk().copy());
			attack = new AnimationController(unitClass.getAttack().copy());
			die =  new AnimationController(unitClass.getDie().copy());
		}

		public AnimationController getStay() {
			return stay;
		}

		public AnimationController getWalk() {
			return walk;
		}

		public AnimationController getAttack() {
			return attack;
		}

		public AnimationController getDie() {
			return die;
		}
		
	}

	public class Steering { 
		
		private Vector2D steeringForce = new Vector2D();
		
		private Vector2D forceSeek = new Vector2D();
		private Vector2D forceFlee = new Vector2D();
		private Vector2D forceArrivel = new Vector2D();
		
		private float slowingDistance = 1;
		
		private float weightSeek = 1;
		private float weightFlee = 1;
		private float weightArrivel = 1;
		
		private Vehicle vehicle = new BasicVehicle() {

			@Override
			public Vector2D getPosition() {
				return Unit.this.position;
			}

			@Override
			public float getMaxSpeed() {
				return Unit.this.getMaxSpeed();
			}
			
			@Override
			public Vector2D getVelocity() {
				return Unit.this.velocity;
			}

			@Override
			public Vector2D getHeading() {
				return Unit.this.heading;
			}

			@Override
			public float getRadius() {
				return Unit.this.getRadiusBounding();
			}
					
		};
		
		public void seek(Vector2D target) {
			forceSeek.set(0, 0);
			SteerLibrary.seek(forceSeek, vehicle, target);
			forceSeek.mult(weightSeek);
			accumulateForce(forceSeek);
		}
		
		public void flee(Vector2D target) {
			forceFlee.set(0, 0);
			SteerLibrary.flee(forceFlee, vehicle, target);
			forceFlee.mult(weightFlee);
			accumulateForce(forceFlee);
		}
		
		public void arrivel(Vector2D target) {
			forceArrivel.set(0, 0);
			SteerLibrary.arrivel(forceArrivel, vehicle, target, slowingDistance);
			forceArrivel.mult(weightArrivel);
			accumulateForce(forceArrivel);
		}
		
		public void avoidance(Unit other) {
			Vector2D force = other.steering.vehicle.steerToAvoid(vehicle);
			accumulateForce(force);
		}
		
		public void braking(float rate) {
			vehicle.applyBrakingForce(rate);
		}
		
		private float getSpeed() {
			return vehicle.getSpeed();
		}
	
		private boolean accumulateForce(Vector2D force) {
			float magnitudeRemannig = vehicle.getMaxForce() - steeringForce.length();
			if (magnitudeRemannig <= 0f) {
				return true;
			}
			float magnitudeToAdd = force.length();
			if (magnitudeToAdd < magnitudeRemannig) {
				steeringForce.add(force);
			} else {
				force.normalise();
				force.mult(magnitudeRemannig);
				steeringForce.add(force);
			}
			return false;
		}
		
		private void updatePosition() {
			vehicle.applySteeringForce(steeringForce);
			steeringForce.set(0, 0);
		}
		
		private boolean enforceNonPenetrationConstraint(Unit other) {
			if (Unit.this == other || other == null) {
				return false;
			}
			float minDistance = Unit.this.getRadiusBounding() + other.getRadiusBounding();
			return SteerLibrary.enforceNonPenetrationConstraint(vehicle, other.steering.vehicle, minDistance);
		}
		
	}
	
}
